package edu.chalmers.AV_2DPlatformerFramework.Model;

import edu.chalmers.AV_2DPlatformerFramework.Utility.Vector2D;
import edu.chalmers.AV_2DPlatformerFramework.Utility.VectorUtility;
import edu.chalmers.AV_2DPlatformerFramework.Utility.Pair;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;

/**
 * Physical representation of objects in game world.
 * Used for detection and handling of collisions.
 * @version 1.1
 * @author Joakim Karlsson, Gustav Vidhög
 * @revised Joakim Karlsson & Gustav Vidhög, 20140407
 * @revised Gustav Vidhög, 20140417
 * @revised Gustav Vidhög, 20140424
 * @revised Joakim Karlsson, Gustav Vidhög, 20140504
 */
public class Hitbox {
    
    /**
     * Path describing geometry of this {@code Hitbox}.
     */
    private Path2D.Double path;
    
    /**
     * Constructs {@code Hitbox} from array of vertices.
     * Builds path by traversing straight lines between the vertices in order
     * and finally closing it, then constructs {@code Hitbox} from path.
     * @param vertices - array of vertices to use to describe the
     * {@code Hitbox}.
     * @throws HitboxMalformedException if new {@code Hitbox} is not
     * well-formed.
     * @author Joakim Karlsson, Gustav Vidhög
     * @version 1.2
     * @since 0.2
     * @revised Gustav Vidhög, 20140417
     * @revised Gustav Vidhög, 20140424
     */
    public Hitbox(Vector2D[] vertices) throws HitboxMalformedException {
        path = buildPath(vertices);
        beWellFormed();
    }
    
    /**
     * Polygon-style constructor.
     * @param xCoords - array of X coordinates.
     * @param yCoords - array of Y coordinates.
     * @param nbrOfPoints - number of vertices in {@code Hitbox}.
     * @throws NegativeArraySizeException if {@code nbrOfPoints} is negative.
     * @throws IndexOutOfBoundsException if there are an insufficient number of
     * coordinates in {@code xCoords} or {@code yCoords} to build a
     * {@code Hitbox} with {@code nbrOfPoints} number of vertices.
     * @throws HitboxMalformedException if new {@code Hitbox} is not
     * well-formed.
     * @author Joakim Karlsson, Gustav Vidhög
     * @version 1.2
     * @since 0.2
     * @revised Gustav Vidhög, 20140417
     * @revised Gustav Vidhög, 20140424
     * @deprecated
     */
    @Deprecated
    public Hitbox(int[] xCoords, int[] yCoords, int nbrOfPoints)
            throws HitboxMalformedException {
        path = buildPath(xCoords, yCoords, nbrOfPoints);
        beWellFormed();
    }
    
    /**
     * Builds a {@code Path2D.Double} from an array of vertices. Path is built
     * by traversing straight lines between the vertices in order and finally
     * closing it.
     * @param vertices - array of vertices to use to describe the path.
     * @return resulting {@code Path2D.Double}.
     * @author Joakim Karlsson, Gustav Vidhög
     * @version 1.2
     * @since 0.2
     * @revised Gustav Vidhög, 20140424
     * @revised Joakim Karlsson, Gustav Vidhög, 20140504
     */
    private static Path2D.Double buildPath(Vector2D[] vertices) {
        Path2D.Double newPath = new Path2D.Double();
        if (vertices == null || vertices.length == 0) {
            return newPath;
        }
        
        newPath.moveTo(vertices[0].getX(), vertices[0].getY());
        for (int i = 1; i < vertices.length; i++) {
            newPath.lineTo(vertices[i].getX(), vertices[i].getY());
        }
        newPath.closePath();
        
        return newPath;
    }
    
    /**
     * Polygon-style buildPath.
     * Path is built by traversing straight lines between vertices described by
     * the X- and Y-coordinates in order and finally closing it.
     * @see @link{buildPath}.
     * @param xCoords - array of X coordinates.
     * @param yCoords - array of Y coordinates.
     * @param nbrOfPoints - number of vertices in path.
     * @return resulting {@code Path2D.Double}.
     * @throws NegativeArraySizeException if {@code nbrOfPoints} is negative.
     * @throws IndexOutOfBoundsException if there are an insufficient number of
     * coordinates in {@code xCoords} or {@code yCoords} to build a path with
     * {@code nbrOfPoints} number of vertices.
     * @author Joakim Karlsson, Gustav Vidhög
     * @version 1.1
     * @since 0.2
     * @revised Joakim Karlsson, Gustav Vidhög, 20140504
     * @deprecated
     */
    @Deprecated
    private static Path2D.Double buildPath(int[] xCoords, int[] yCoords,
            int nbrOfPoints) {
        if (nbrOfPoints < 0) {
            throw new NegativeArraySizeException();
        } else if (nbrOfPoints > xCoords.length ||
                nbrOfPoints > yCoords.length) {
            throw new IndexOutOfBoundsException();
        }
        Vector2D[] points = new Vector2D[nbrOfPoints];
        for (int i = 0; i < nbrOfPoints; i++) {
            points[i] = new Vector2D((double) xCoords[i],
                    (double) yCoords[i]);
        }
        return buildPath(points);
    }
    
    /**
     * Retrieves the data from a {@code PathIterator} describing the geometry
     * of this {@code Hitbox} and returns it stored in a random-access list.
     * @return list of {@code Pair}s of the (wrapped) integer type values and
     * the (wrapped) coordinate data provided by the {@code PathIterator} of
     * this {@code Hitbox}.
     * @author Gustav Vidhög
     * @version 1.2
     * @since 0.3
     * @revised Gustav Vidhög, 20140424
     * @revised Joakim Karlsson, Gustav Vidhög, 20140504
     */
    private ArrayList<Pair<Integer, Double[]>> toList () {
        ArrayList<Pair<Integer, Double[]>> data = new ArrayList<>();
        int typeData;
        double[] coordData = new double[6];
        ArrayList<Double[]> wrappedCoordData = new ArrayList<>();
        PathIterator iter = path.getPathIterator(new AffineTransform());
        for (int i = 0; !iter.isDone(); i++) {
            typeData = iter.currentSegment(coordData);
            wrappedCoordData.add(new Double[6]);
            for (int j = 0; j < 6; j++) {
                wrappedCoordData.get(i)[j] = new Double(coordData[j]);
            }
            data.add(new Pair<>(new Integer(typeData),
                    wrappedCoordData.get(i)));
            iter.next();
        }
        return data;
    }
    
    /**
     * Throws an exception if {@code Hitbox} is not well-formed. Tests whether
     * the underlying path describing the {@code Hitbox} exterior is
     * well-formed after several criteria: It forms a simply connected closed
     * curve, begins with a special "move" segment, contains at least four
     * segments (corresponding to three vertices), all edges between vertices
     * are straight lines, and no such edge intersects another edge.
     * @throws HitboxMalformedException if {@code Hitbox} is not well-formed.
     * @author Gustav Vidhög
     * @version 1.0
     * @since 0.3
     */
    private void beWellFormed () throws HitboxMalformedException {
        ArrayList<Pair<Integer, Double[]>> list = toList();
        int s = list.size();
        if (s < 4) {
            throw new HitboxMalformedException(
                    "edu.chalmers.AV_2DPlatformerFramework.Hitbox" +
                            ".beWellFormed: Path does not contain at least " +
                            "four segments.");
        }
        if (list.get(0).getFirst().intValue() != PathIterator.SEG_MOVETO) {
            throw new HitboxMalformedException(
                    "edu.chalmers.AV_2DPlatformerFramework.Hitbox." +
                            ".beWellFormed: Path does not begin with move " +
                            "segment.");
        } else if (list.get(s-1).getFirst().intValue() !=
                PathIterator.SEG_CLOSE) {
            throw new HitboxMalformedException(
                    "edu.chalmers.AV_2DPlatformerFramework.Hitbox." +
                            "beWellFormed: Path not simply connected.");
        } else {
            for (int i = 1; i < s-1; i++) {
                if (list.get(i).getFirst().intValue() !=
                        PathIterator.SEG_LINETO) {
                    throw new HitboxMalformedException(
                            "edu.chalmers.AV_2DPlatformerFramework.Hitbox." +
                                    "beWellFormed: Path not consisting of " +
                                    "straight lines");
                }
            }
            Line2D.Double[] edges = getEdges(); //Safe at this point.
            for (int i = 0; i < edges.length; i++) {
                for (int j = 0; j < i-1; j++) {
                    if ((! (j == 0 && i == edges.length-1)) &&
                            edges[i].intersectsLine(edges[j])) {
                        throw new HitboxMalformedException(
                                "edu.chalmers.AV_2DPlatformerFramework." +
                                        "Hitbox.beWellFormed: Path " +
                                        "contains intersecting edges");
                    }
                }
            }
        }
    }
    
    /**
     * Returns vertices of {@code Hitbox}. Works on the assumption that
     * {@code Hitbox] is well-formed, as constructors and mutators able to
     * malform it assert this fact.
     * @return array containing {@code Hitbox} vertices.
     * @author Gustav Vidhög
     * @version 1.0
     * @since 0.3
     */
    public Vector2D[] getVertices() {
        ArrayList<Pair<Integer, Double[]>> list = toList();
        int s = list.size();
        Vector2D[] vertices = new Vector2D[s-1];
        double[] coordData = new double[6];
        for (int i = 0; i < s-1; i++) {
            for (int j = 0; j < 6; j++) {
                coordData[j] = list.get(i).getSecond()[j].doubleValue();
            }
            vertices[i] = new Vector2D(coordData[0], coordData[1]);
        }
        return vertices;
    }
    
    /**
     * Returns edges of {@code Hitbox}. Works on the assumption that
     * {@code Hitbox} is well-formed, as constructors and mutators able to
     * malform it assert this fact.
     * @return array containing {@code Hitbox} edges.
     * @author Joakim Karlsson, Gustav Vidhög
     * @version 2.0
     * @since 0.2
     * @revised Gustav Vidhög, 20140417
     */
    public Line2D.Double[] getEdges() {
        Vector2D[] vertices = getVertices();
        Line2D.Double[] edges = new Line2D.Double[vertices.length];
        for (int i = 0; i < vertices.length; i++) {
            edges[i] = new Line2D.Double(vertices[i], vertices[(i+1) %
                    vertices.length]);
        }
        return edges;
    }

    /**
     * Translates the vertices of this {@code Hitbox} by a vector {@code diff}.
     * @param diff - vector to be added to every vertex of {@code Hitbox}.
     * @author Gustav Vidhög
     * @version 1.2
     * @since 0.3
     * @revised Gustav Vidhög, 20140424
     * @revised Joakim Karlsson, 20140507
     */
    public void translate (Vector2D diff) {
        Vector2D[] vertices = getVertices();       
        for(int i = 0; i < vertices.length; i++) {
            vertices[i] = VectorUtility.add(vertices[i], diff);
        }
        
        path.reset();
        path.moveTo(vertices[0].x, vertices[0].y);
        for (int i = 1; i < vertices.length; i++) {
            path.lineTo(vertices[i].x, vertices[i].y);
        }
        path.closePath();
    }
    
    /**
     * Polygon-style translate.
     * @see @link{translate}.
     * @param deltaX - X-coordinate value to be added to every vertex of
     * {@code Hitbox}.
     * @param deltaY - Y-coordinate value to be added to every vertex of
     * {@code Hitbox}.
     * @author Gustav Vidhög
     * @version 1.0
     * @since 0.3
     * @deprecated
     */
    @Deprecated
    public void translate(int deltaX, int deltaY) {
        translate(new Vector2D(deltaX, deltaY));
    }
    
    /**
     * Returns rectangular bounding box for this {@code Hitbox}.
     * @return rectangular bounding box.
     * @author Gustav Vidhög
     * @version 1.0
     * @since 1.0
     */
    public Rectangle2D getBounds2D() {
        return path.getBounds2D();
    }
    
    /**
     * Tests whether the bounding box of this {@code Hitbox} overlaps that of
     * another {@code Hitbox}. Formally tests if the {@code Hitbox}es intersect
     * or if either of them contains the other one.
     * @param hitbox to be tested for overlapping.
     * @return true if {@code Hitbox}es overlap; false otherwise.
     * @author Gustav Vidhög
     * @version 1.1
     * @since 1.0
     * @revised Joakim Karlsson 20140505
     */
    public boolean overlaps(Hitbox hitbox) {
        return (path.getBounds2D().intersects(hitbox.getBounds2D()) ||
                path.getBounds2D().contains(hitbox.getBounds2D()) ||
                hitbox.path.getBounds2D().contains(getBounds2D()));
    }
    
    /**
     * Creates and returns a deep copy of this {@code Hitbox}. For any
     * {@code Hitbox} {@code h}, the expression:
     * <blockquote>
     * <pre>
     * h.copy() != h</pre></blockquote>
     * and the expression:
     * <blockquote>
     * <pre>
     * x.copy().equals(x)</pre></blockquote>
     * are both {@code true}.
     * @return a deep copy of this {@code Hitbox}.
     * @author Gustav Vidhög
     * @version 1.0
     * @since 0.3
     */
    public Hitbox copy() {
        Hitbox copy = null;
        try {
            copy = new Hitbox(getVertices());
            
        } catch (HitboxMalformedException e) {
            throw new IllegalStateException(
                    "edu.chalmers.AV_2DPlatformerFramework.Hitbox.copy: " +
                            "Invoked on malformed Hitbox.", e);
        }
        return copy;
    }
    
    /**
     * Indicates whether some other object is "equal to" this {@code Hitbox}.
     * @param obj - the reference {@code Object} with which to compare.
     * @return {@code true} if this {@code Hitbox} is the same as the
     * {@code obj} argument; {@code false} otherwise.
     * @author Gustav Vidhög
     * @version 1.0
     * @since 0.3
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        } else if (obj == null || obj.getClass() != this.getClass()) {
            return false;
        } else {
            Hitbox hitbox = (Hitbox) obj;
            Vector2D[] vertices = getVertices();
            Vector2D[] hitboxVertices = hitbox.getVertices();
            if (vertices.length != hitboxVertices.length) {
                return false;
            } else {
                for (int i = 0; i < vertices.length; i++) {
                    if (! vertices[i].equals(hitboxVertices[i])) {
                        return false;
                    }
                }
                return true;
            }
        }
    }
    
    /**
     * Returns a hash code value for the {@code Hitbox}.
     * @return a hash code value for this {@code Hitbox}.
     * @author Gustav Vidhög
     * @version 1.0
     * @since 0.3
     */
    @Override
    public int hashCode() {
        final int prime = 8191; //Fifth Mersenne prime, 2^13-1.
        int hash = 1;
        Vector2D[] vertices = getVertices();
        for (Vector2D vertex : vertices) {
            hash = hash * prime + vertex.hashCode();
        }
        return hash;
    }
    
    /**
     * Signals that a {@code Hitbox} constructed or mutated by some method is
     * malformed.
     * @author Gustav Vidhög
     * @version 1.0
     * @since 0.3
     */
    public class HitboxMalformedException extends Exception {
        /**
         * Constructs a {@code HitboxMalformedException} with no detail
         * message.
         * @author Gustav Vidhög
         * @version 1.0
         * @since 0.3
         */
        public HitboxMalformedException() {
            super();
        }
        
        /**
         * Constructs a {@code HitboxMalformedException} with the specified
         * detail message. A detail message is a {@code String} that describes
         * this particular exception.
         * @param message - the {@code String} containing a detail message.
         * @author Gustav Vidhög
         * @version 1.0
         * @since 0.3
         */
        public HitboxMalformedException(String message) {
            super(message);
        }
    }
}
